﻿using Bilibili.Common;
using Bilibili.Common.Api;
using Bilibili.Common.Danmaku.Converter;
using Bilibili.Common.Log;
using Bilibili.Model.Common;
using Bilibili.Model.Danmaku;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using PCLUntils.Json;
using PCLUntils.Objects;
using PCLUntils.Steams;
using System;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace Bilibili.WebClient
{
    public abstract class WebSocketBase : IDisposable
    {
        private int RoomId { get; }
        private string[] DefaultHosts { get; }
        private string ChatHost = "chat.bilibili.com";
        private int ChatPort { get; set; } = 2243;
        private TcpClient TcpClient { get; }
        private Stream NetStream { get; set; }
        private bool Connected { get; set; } = false;
        public event MessageRecive OnMessageRecive;
        protected abstract IApiClient ApiClient { get; }
        public WebSocketBase(int roomid)
        {
            RoomId = roomid;
            TcpClient = new TcpClient();
            DefaultHosts = new string[] { "livecmt-2.bilibili.com", "livecmt-1.bilibili.com" };
        }
        public async Task<bool> ConnectAsync()
        {
            try
            {
                var token = string.Empty;
                try
                {
                    var req = await ApiClient.GetResults("https://api.live.bilibili.com/room/v1/Danmu/getConf?room_id=" + RoomId);
                    var roomobj = req.ToJObject();
                    if (roomobj.IsNotEmpty())
                    {
                        token = roomobj["data"]["token"].ToString();
                        ChatHost = roomobj["data"]["host"].ToString();
                        ChatPort = roomobj["data"]["port"].Value<int>();
                        if (string.IsNullOrEmpty(ChatHost))
                            throw new Exception();
                    }
                }
                catch
                {
                    ChatHost = DefaultHosts[new Random().Next(DefaultHosts.Length)];
                }
                var ipaddrss = await Dns.GetHostAddressesAsync(ChatHost);
                var random = new Random();
                var idx = random.Next(ipaddrss.Length);
                await TcpClient.ConnectAsync(ipaddrss[idx], ChatPort);
                NetStream = Stream.Synchronized(TcpClient.GetStream());
                if (await SendJoinChannel(RoomId, token))
                {
                    Connected = true;
                    _ = HeartbeatLoop();
                    _ = ReceiveMessageLoop();
                    return true;
                }
                return false;
            }
            catch (Exception ex)
            {
                LogManager.Instance.LogError("ConnectAsync", ex);
                return false;
            }
        }
        private async Task ReceiveMessageLoop()
        {
            try
            {
                var stableBuffer = new byte[16];
                var buffer = new byte[4096];
                while (Connected)
                {
                    try
                    {
                        await NetStream.ReadBAsync(stableBuffer, 0, 16);
                        var protocol = DanmakuProtocol.FromBuffer(stableBuffer);
                        if (protocol.PacketLength < 16)
                            break;
                        var payloadlength = protocol.PacketLength - 16;
                        if (payloadlength == 0) continue;
                        buffer = new byte[payloadlength];
                        await NetStream.ReadBAsync(buffer, 0, payloadlength);
                        ProcessDanmaku(protocol, buffer);
                    }
                    catch (Exception ex)
                    {
                        LogManager.Instance.LogError("ReceiveMessageLoop", ex);
                    }
                }
            }
            catch (Exception ex)
            {
                LogManager.Instance.LogError("ReceiveMessageLoop", ex);
                Disconnect();
            }
        }
        private void ProcessDanmaku(DanmakuProtocol protocol, byte[] buffer)
        {
            switch (protocol.Action)
            {
                case 5:
                    {
                        string json = TryGetJson(ref buffer);
                        if (json.Trim().Length != 0)
                        {
                            json = "[" + json + "]";
                            JArray jarray = JArray.Parse(json);
                            foreach (var obj in jarray)
                            {
                                if (obj["cmd"].ToString().Contains("DANMU_MSG"))
                                {
                                    var v = new DanmuMsg();
                                    if (obj["info"] != null && obj["info"].ToArray().Length != 0)
                                    {
                                        v.Text = obj["info"][1].ToString();
                                        if (obj["info"][2] != null && obj["info"][2].ToArray().Length != 0)
                                        {
                                            v.UserName = obj["info"][2][1].ToString() + ":";
                                            if (obj["info"][2][3] != null && Convert.ToInt32(obj["info"][2][3].ToString()) == 1)
                                            {
                                                v.Vip = "老爷";
                                                v.IsVip = true;
                                            }
                                            if (obj["info"][2][4] != null && Convert.ToInt32(obj["info"][2][4].ToString()) == 1)
                                            {
                                                v.Vip = "年费老爷";
                                                v.IsVip = false;
                                                v.IsBigVip = true;
                                            }
                                            if (obj["info"][2][2] != null && Convert.ToInt32(obj["info"][2][2].ToString()) == 1)
                                            {
                                                v.Vip = "房管";
                                                v.IsAdmin = true;
                                            }
                                        }
                                        if (obj["info"][3] != null && obj["info"][3].ToArray().Length != 0)
                                        {
                                            v.Medal_Name = obj["info"][3][1].ToString();
                                            v.Medal_Lv = obj["info"][3][0].ToString();
                                            v.MedalColor = obj["info"][3][4].ToString();
                                            v.HasMedal = true;
                                        }
                                        if (obj["info"][4] != null && obj["info"][4].ToArray().Length != 0)
                                        {
                                            v.Ul = "UL" + obj["info"][4][0].ToString();
                                            v.UlColor = obj["info"][4][2].ToString();
                                        }
                                        if (obj["info"][5] != null && obj["info"][5].ToArray().Length != 0)
                                        {
                                            v.User_Title = obj["info"][5][0].ToString();
                                            v.HasTitle = true;
                                        }
                                        OnMessageRecive?.Invoke(new LiveDanmu() { Type = LiveDanmuTypes.Danmu, Value = v });
                                    }
                                }
                                switch (obj["cmd"].ToString())
                                {
                                    case "SEND_GIFT":
                                        var g = new GiftMsg();
                                        if (obj["data"] != null)
                                        {
                                            g.Uname = obj["data"]["uname"].ToString();
                                            g.Action = obj["data"]["action"].ToString();
                                            g.GiftId = Convert.ToInt32(obj["data"]["giftId"].ToString());
                                            g.GiftName = obj["data"]["giftName"].ToString();
                                            g.Num = obj["data"]["num"].ToString();
                                            g.Uid = obj["data"]["uid"].ToString();
                                            OnMessageRecive?.Invoke(new LiveDanmu() { Type = LiveDanmuTypes.Gift, Value = g });
                                        }
                                        break;
                                    case "WELCOME":
                                        var w = new WelcomeMsg();
                                        if (obj["data"] != null)
                                        {
                                            w.Uname = obj["data"]["uname"].ToString();
                                            w.Uid = obj["data"]["uid"].ToString();
                                            w.Svip = obj["data"]["vip"].ToInt32() != 1;
                                            OnMessageRecive?.Invoke(new LiveDanmu() { Type = LiveDanmuTypes.Welcome, Value = w });
                                        }
                                        break;
                                    case "SYS_MSG":
                                        if (obj["msg"] != null)
                                            OnMessageRecive?.Invoke(new LiveDanmu() { Type = LiveDanmuTypes.SystemMsg, Value = obj["msg"].ToString() });
                                        break;
                                }
                            }
                        }
                        break;
                    }
            }
        }
        private string TryGetJson(ref byte[] buffer)
        {
            string json;
            try
            {
                using MemoryStream outBuffer = new MemoryStream();
                using (DeflateStream compressedzipStream = new DeflateStream(new MemoryStream(buffer, 2, buffer.Length - 2), CompressionMode.Decompress))
                {
                    byte[] block = new byte[1024];
                    while (true)
                    {
                        int bytesRead = compressedzipStream.Read(block, 0, block.Length);
                        if (bytesRead <= 0)
                            break;
                        else
                            outBuffer.Write(block, 0, bytesRead);
                    }
                    compressedzipStream.Close();
                    buffer = outBuffer.ToArray();
                }
                json = Regex.Replace(Encoding.UTF8.GetString(buffer, 16, buffer.Length - 16), "}\\0\\0.*?\\0\\0{", "},{");
            }
            catch
            {
                json = Encoding.UTF8.GetString(buffer, 0, buffer.Length);
            }
            return json;
        }
        private async Task HeartbeatLoop()
        {

            try
            {
                while (Connected)
                {
                    await SendHeartbeatAsync();
                    await Task.Delay(30000);
                }
            }
            catch { Disconnect(); }
        }
        public void Disconnect()
        {
            try
            {
                if (Connected)
                {
                    Connected = false;
                    TcpClient.Close();
                    NetStream.Close();
                    TcpClient.Dispose();
                    NetStream = null;
                    GC.Collect(0);
                }
            }
            catch { }
        }
        private async Task SendHeartbeatAsync()
        {
            await SendSocketDataAsync(2);
        }
        Task SendSocketDataAsync(int action, string body = "")
        {
            return SendSocketDataAsync(0, 16, 2, action, 1, body);
        }
        async Task SendSocketDataAsync(int packetlength, short magic, short ver, int action, int param = 1, string body = "")
        {
            try
            {
                var playload = Encoding.UTF8.GetBytes(body);
                if (packetlength == 0)
                    packetlength = playload.Length + 16;
                var buffer = new byte[packetlength];
                using var ms = new MemoryStream(buffer);
                var b = EndianBitConverter.BigEndian.GetBytes(buffer.Length);
                await ms.WriteAsync(b, 0, 4);
                b = EndianBitConverter.BigEndian.GetBytes(magic);
                await ms.WriteAsync(b, 0, 2);
                b = EndianBitConverter.BigEndian.GetBytes(ver);
                await ms.WriteAsync(b, 0, 2);
                b = EndianBitConverter.BigEndian.GetBytes(action);
                await ms.WriteAsync(b, 0, 4);
                b = EndianBitConverter.BigEndian.GetBytes(param);
                await ms.WriteAsync(b, 0, 4);
                if (playload.Length > 0)
                    await ms.WriteAsync(playload, 0, playload.Length);
                await NetStream.WriteAsync(buffer, 0, buffer.Length);
            }
            catch (Exception ex)
            {
                LogManager.Instance.LogError("SendSocketDataAsync", ex);
            }
        }
        private async Task<bool> SendJoinChannel(int channelId, string token)
        {
            try
            {
                var packetModel = new { roomid = channelId, uid = 0, protover = 2, token, platform = "danmuji" };
                var playload = JsonConvert.SerializeObject(packetModel);
                await SendSocketDataAsync(7, playload);
                return true;
            }
            catch (Exception ex)
            {
                LogManager.Instance.LogError("SendJoinChannel", ex);
                return false;
            }
        }
        public void Dispose()
        {
            try
            {
                Disconnect();
            }
            catch { }
        }
    }
}